<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
    <title></title>
</head>
<body>

<script>
  const data = {foo: 1, bar: 2}

  function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
      effectFn.deps[i].delete(effectFn)
    }

    effectFn.deps.length = 0
  }

  let activeEffect
  const effectsStack = []
  const effect = (fn, options = {}) => {
    const effectFn = () => {
      cleanup(effectFn)
      activeEffect = effectFn
      effectsStack.push(effectFn)
      const res = fn()
      effectsStack.pop()
      activeEffect = effectsStack[effectsStack.length - 1]
      return res
    }
    effectFn.deps = []
    effectFn.options = options
    if (!options.lazy) {
      effectFn()
    }

    return effectFn
  }

  const bucket = new WeakMap()

  function track(target, key) {
    if (!activeEffect) return
    let depsMap = bucket.get(target)
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }

    let deps = depsMap.get(key)
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)
    activeEffect.deps.push(deps)
  }

  function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) {
      return
    }
    const effects = depsMap.get(key)
    const effectsToRun = new Set()   // 防止死循环
    effects && effects.forEach(effectFn => {
      if (effectFn !== activeEffect) {  // 避免无限递归
        effectsToRun.add(effectFn)
      }
    })
    effectsToRun && effectsToRun.forEach(effectFn => {
      if (effectFn.options.scheduler) {
        effectFn.options.scheduler(effectFn)
      } else {
        effectFn()
      }
    })
  }

  const obj = new Proxy(data, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, newValue) {
      target[key] = newValue
      trigger(target, key)
      return true
    }
  })

  function computed(getter) {
    let value
    let dirty = true
    const effectFn = effect(getter, {
      lazy: true,
      scheduler() {
        if (!dirty) {
          dirty = true
          trigger(obj, 'value')
        }
      }
    })
    const obj = {
      get value() {
        if (dirty) {
          value = effectFn()
          dirty = false
        }
        track(obj, 'value')
        return value
      }
    }

    return obj
  }

  const sumRes = computed(() => obj.foo + obj.bar)
  effect(() => {
    document.body.innerText = sumRes.value
  })
  console.log(sumRes.value)
  console.log(sumRes.value)
  setTimeout(() => {
    obj.foo++
    console.log(sumRes.value)
  }, 1000)
</script>
</body>
</html>
